﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using System.Text;
using NHibernate.Criterion;
using NHibernate.LambdaExtensions;
using NHibernate.Sidekick.Data.Domain;
using NHibernate.Validator.Engine;
using NHibernate.Validator.Exceptions;

namespace NHibernate.Sidekick.Data.NHibernate
{
    /// <summary>
    /// Provides database operations for the specified type.
    /// </summary>
    /// <typeparam name="T">The specified type.</typeparam>
    /// <typeparam name="TId">The type of the id.</typeparam>
    public class Dao<T, TId> : IDao<T,TId> where T: DomainObject<T, TId>
    {
        #region NHibernate Validator Engine
        private static ValidatorEngine _validatorEngine;
        private static ValidatorEngine ValidatorEngine
        {
            get { return _validatorEngine ?? (ValidatorEngine = new ValidatorEngine()); }
            set { _validatorEngine = value; }
        }

        private static void ValidateEntityValues(T entity)
        {
            if (!ValidatorEngine.IsValid(entity))
            {
                var invalidValues = ValidatorEngine.Validate(entity);
                var message = new StringBuilder();
                foreach (var invalidValue in invalidValues)
                {
                    message.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0}: {1}",
                                                     invalidValue.PropertyName,
                                                     invalidValue.Message));
                }
                throw new HibernateValidatorException(message.ToString());
            }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the Dao class.
        /// </summary>
        public Dao()
        {
            dataOperationsDictionary = new Dictionary<DataOperation, Action<T>>
                                           {
                                               {DataOperation.Delete, t => Delete(t)},
                                               {DataOperation.Refresh, t => Refresh(t)},
                                               {DataOperation.Save, t => Save(t)},
                                               {DataOperation.SaveOrUpdate, t => SaveOrUpdate(t)},
                                               {DataOperation.Update, t => Update(t)}
                                           };
        }

        private readonly IDictionary<DataOperation, Action<T>> dataOperationsDictionary;

        /// <summary>
        /// Used to obtain the <see cref="System.Type"/> object for a type.
        /// </summary>
        /// <value>The type.</value>
        private static System.Type PersitentType
        {
            get { return typeof (T); }
        }

        /// <summary>
        /// Gets the main runtime interface between a .NET application and NHibernate.
        /// </summary>
        /// <value>The main <see cref="ISession"/>.</value>
        private static ISession NHibernateSession
        {
            get { return SessionManager.Instance.GetSession(); }
        }

        /// <summary>
        /// Returns the persistent instance with the specified identity.
        /// </summary>
        /// <param name="id">The instance's identifier.</param>
        /// <returns>The persistent instance, or null if there is no such persistent instance.</returns>
        public T GetById(TId id)
        {
            return GetById(id, false);
        }

        /// <summary>
        /// Returns the persistent instance with the specified identity.
        /// </summary>
        /// <param name="id">The instance's identifier.</param>
        /// <param name="shouldLock">If set to <c>true</c>, lock the instace.</param>
        /// <returns>The persistent instance, or null if there is no such persistent instance.</returns>
        public T GetById(TId id, bool shouldLock)
        {
            T entity;

            if (shouldLock)
            {
                entity = (T) NHibernateSession.Load(PersitentType, id, LockMode.Upgrade);
            }
            else
            {
                entity = (T) NHibernateSession.Load(PersitentType, id);
            }

            return entity;
        }

        /// <summary>
        /// Loads every instance of the requested type using the supplied <see cref="ICriterion"/>.
        /// If no <see cref="ICriterion"/> is supplied, this behaves like <see cref="GetAll"/>.
        /// </summary>
        /// <returns>Every entity available for the requested type.</returns>
        public List<T> GetAll()
        {
            return GetByCriteria(null,null);
        }

        /// <summary>
        /// Loads every instance of the requested type using the supplied <see cref="ICriterion"/>.
        /// If no <see cref="ICriterion"/> is supplied, this behaves like <see cref="GetAll"/>.
        /// </summary>
        /// <param name="maxNumberOfResults">The maxinum number of results.</param>
        /// <param name="firstResult">Accept results following this one.</param>
        /// <param name="expressions">The filtering expressions.</param>
        /// <returns>The collection of entities.</returns>
        public List<T> GetByCriteria(int? maxNumberOfResults, int? firstResult, params Expression<Func<T, bool>>[] expressions)
        {
            var criteria = NHibernateSession.CreateCriteria(PersitentType);

            if (maxNumberOfResults.HasValue)
            {
                criteria.SetMaxResults(maxNumberOfResults.Value);
            }

            if (firstResult.HasValue)
            {
                criteria.SetFirstResult(firstResult.Value);
            }
            
            foreach (var expression in expressions)
            {
                criteria.Add(expression);
            }

            return criteria.List<T>() as List<T>;
        }


        /// <summary>
        /// Loads a unique instance of the requested type using the supplied <see cref="ICriterion"/>.
        /// If no <see cref="ICriterion"/> is supplied, this behaves like <see cref="GetAll"/>.
        /// </summary>
        /// <param name="expressions">Evaluation expressions.</param>
        /// <returns>The unique instance.</returns>
        public T GetUniqueByExpression(params Expression<Func<T, bool>>[] expressions)
        {
            var criteria = NHibernateSession.CreateCriteria(PersitentType);

            foreach (var expression in expressions)
            {
                criteria.Add(expression);
            }

            return criteria.UniqueResult<T>();
        }

        /// <summary>
        /// Operates the collection of entities equally.
        /// </summary>
        /// <param name="dataOperation">The data operation.</param>
        /// <param name="entities">The entities.</param>
        public void OperateCollectionEqually(DataOperation dataOperation, params T[] entities)
        {
            foreach (var entity in entities)
            {
                dataOperationsDictionary[dataOperation].Invoke(entity);
            }
        }

        /// <summary>
        /// Operates the collection of entities differently.
        /// </summary>
        /// <param name="entityDictionary">The entity dictionary.</param>
        public void OperateCollectionDifferently(Dictionary<T, DataOperation> entityDictionary)
        {
            foreach (var entity in entityDictionary)
            {
                dataOperationsDictionary[entity.Value].Invoke(entity.Key);
            }
        }

        /// <summary>
        /// Persist the given transient instance, first assigning a generated identifier.
        /// </summary>
        /// <param name="entity">The transient entity.</param>
        /// <returns>The saved instance.</returns>
        public T Save(T entity)
        {
            ValidateEntityValues(entity);
            NHibernateSession.Save(entity);
            return entity;
        }

        /// <summary>
        /// Update the persistent instance with the identifier of the given detached instance.
        /// </summary>
        /// <param name="entity">The transient entity.</param>
        /// <returns>The updated instance.</returns>
        public T Update(T entity)
        {
            ValidateEntityValues(entity);
            NHibernateSession.Update(entity);
            return entity;
        }

        /// <summary>
        /// Either <see cref="Save(T)"/> or <see cref="Update(T)"/> the given instance, depending upon resolution of the unsaved-value checks
        /// </summary>
        /// <param name="entity">A transient or detached instance containing new or updated state .</param>
        /// <returns>The operated instance.</returns>
        public T SaveOrUpdate(T entity)
        {
            ValidateEntityValues(entity);
            NHibernateSession.SaveOrUpdate(entity);
            return entity;
        }

        /// <summary>
        /// Remove a persistent instance from the datastore. The argument may be an instance associated
        /// with the receiving Session or a transient instance with an identifier associated with existing
        /// persistent state. This operation cascades to associated instances if the association is mapped
        /// with cascade="delete". 
        /// </summary>
        /// <param name="entity">The instance to be removed.</param>
        public void Delete(T entity)
        {
            NHibernateSession.Delete(entity);
        }

        /// <summary>
        /// Remove a persistent instance from the datastore. The argument may be an instance associated
        /// with the receiving Session or a transient instance with an identifier associated with existing
        /// persistent state. This operation cascades to associated instances if the association is mapped
        /// with cascade="delete". 
        /// </summary>
        /// <param name="id">The instance's identifier.</param>
        public void Delete(TId id)
        {
            var entity = GetById(id, false);
            NHibernateSession.Delete(entity);
        }

        /// <summary>
        /// Flush the associated ISession and end the unit of work.
        /// </summary>
        public void CommitChanges()
        {
            if (SessionManager.Instance.HasOpenTransaction())
            {
                SessionManager.Instance.CommitTransaction();
            }
            else
            {
                // If there's no transaction, just flush the changes
                SessionManager.Instance.GetSession().Flush();
            }
        }

        /// <summary>
        /// Forces the underlying <see cref="ITransaction"/> to roll back.
        /// </summary>
        public void RollBackChanges()
        {
            if (SessionManager.Instance.HasOpenTransaction())
            {
                SessionManager.Instance.RollbackTransaction();
            }
        }

        /// <summary>
        /// Re-read the state of the given instance from the underlying database.
        /// </summary>
        /// <param name="entity">The persistent instance.</param>
        public void Refresh(T entity)
        {
            SessionManager.Instance.GetSession().Refresh(entity);
        }

        /// <summary>
        /// Re-reads the state of a collection of instances from the underlying database.
        /// </summary>
        /// <param name="enumerable">The collection of instances.</param>
        public void Refresh(IEnumerable<T> enumerable)
        {
            foreach (var entity in enumerable)
            {
                SessionManager.Instance.GetSession().Refresh(entity);
            }
        }
    }
}
